use crate::{ffi::*, get_funcs, set_funcs};
use std::{ffi::{CString, CStr}, collections::HashMap};
// use libc::{c_void, size_t, c_int, c_char, ptrdiff_t, c_double};
use libc::{c_int, c_void};

pub type GetterFn = fn(*const std::ffi::c_void, &String) -> Value;
pub type GetterMap = HashMap<String, GetterFn>;

pub type SetterFn = fn(*const std::ffi::c_void, &String, &Value);
pub type SetterMap = HashMap<String, SetterFn>;

#[derive(Debug)]
pub enum LuaNumber {
    Float(f64),
    Integer(i64)
}

#[derive(Debug)]
pub enum Value {
    Nil(),
    LightUserdata(*mut c_void),
    Object((*mut c_void, &'static str)),
    Number(LuaNumber)
}

pub trait ToLuaValue {
    fn to_lua_value(&mut self) -> Value {
        Value::LightUserdata(self as *mut Self as *mut std::ffi::c_void)
    }
}

pub struct LuaEngine {
    state: *mut lua_State,
    pub getter_map: GetterMap,
    pub setter_map: SetterMap,
    valid: bool,
    debug_stack: bool
}

pub fn get_table_string(state: *mut lua_State, name: &str, index: i32) -> *const libc::c_char {
    unsafe {
        if index != 0 {
            lua_pushvalue(state, index);
        }
        let c_string: CString = CString::new(name).expect("");
        lua_getfield(state, -2, c_string.as_ptr());

        let string = lua_tolstring(state,  -1, std::ptr::null_mut());
        lua_settop(state,  -2);

        if index != 0 {
            lua_settop(state, -2);
        }

        string
    }
} 

pub fn get_table_cfunction(state: *mut lua_State, name: &str, index: i32) -> lua_CFunction {
    unsafe {
        if index != 0 {
            lua_pushvalue(state, index);
        }
        
        let c_string: CString = CString::new(name).expect("");
        lua_getfield(state, -2, c_string.as_ptr());

        let func = lua_tocfunction(state,  -1);
        lua_settop(state,  -2);

        if index != 0 {
            lua_settop(state, -2);
        }

        func
    }
} 

impl LuaEngine {
    pub fn new() -> Self {
        unsafe {
            let state = luaL_newstate();
            luaL_openlibs(state);

            LuaEngine { 
                state: state, 
                getter_map: HashMap::<String, GetterFn>::new(),
                setter_map: HashMap::<String, SetterFn>::new(),
                valid: true, 
                debug_stack: false 
            }
        }
    }

    /// !!!Why We Need a new init() fn
    /// before this, i push userdata in new(), but the var in new() is a temp_var
    pub fn init(&mut self) {
        unsafe {
            // Add getter map to global
            lua_pushlightuserdata(self.state, &mut self.getter_map as *mut GetterMap as *mut c_void);
            let c_string = CString::new("getter_map").unwrap();
            lua_setglobal(self.state, c_string.as_ptr());

            // Add setter map to global
            lua_pushlightuserdata(self.state, &mut self.setter_map as *mut SetterMap as *mut c_void);
            let c_string = CString::new("setter_map").unwrap();
            lua_setglobal(self.state, c_string.as_ptr());
        }

        println!("[Rust] Address getter_map: {:?}", &mut self.getter_map as *mut GetterMap as *mut c_void);
        println!("[Rust] Address setter_map: {:?}", &mut self.setter_map as *mut SetterMap as *mut c_void);
    }

    fn is_valid(&self) -> bool {
        return self.valid;
    }
    fn is_debug_stack(&self) -> bool {
        return self.debug_stack;
    }
    fn debug_dump(&self) {
        if self.is_debug_stack() {
            self.dump_stack();
        }
    }

    pub fn getter_map_add(&mut self, name: String, func: GetterFn) {
        self.getter_map.insert(name, func);
    }
    pub fn setter_map_add(&mut self, name: String, func: SetterFn) {
        self.setter_map.insert(name, func);
    }

    pub fn pop_stack(&self, index: i32) {
        unsafe {
            lua_settop(self.state, -index-1);
            self.debug_dump();
        }
    }

    pub fn dump_stack(&self) {
        if !self.is_valid() {
            println!("Invalid");
            return;
        }

        unsafe {
            let stack_top = lua_gettop(self.state);

            println!("Dump Stack");

            for i in 1..=stack_top {
                let element_type = lua_type(self.state, i);
                let type_str;
                let output;
                match element_type {
                    0 => {
                        output = CString::new("nil").unwrap();
                        type_str = String::from("nil            ");
                    },
                    1 => {
                        output = CString::new(lua_toboolean(self.state, i).to_string()).unwrap();
                        type_str = String::from("boolean        ");
                    },
                    2 => {
                        output = CString::new((lua_touserdata(self.state, i) as usize).to_string()).unwrap();
                        type_str = String::from("lightuserdata  ");
                    },
                    3 => {
                        let is_integer = lua_isinteger(self.state, i) != 0;
                        if is_integer {
                            output = CString::new(lua_tointegerx(self.state, i, std::ptr::null_mut()).to_string() + &is_integer.to_string()).unwrap();
                        } else {
                            output = CString::new(lua_tonumberx(self.state, i, std::ptr::null_mut()).to_string() + &is_integer.to_string()).unwrap();
                        }
                        type_str = String::from("number         ");
                    },
                    4 => {
                        output = CStr::from_ptr(lua_tolstring(self.state, i, std::ptr::null_mut())).to_owned();
                        type_str = String::from("string         ");
                    },
                    5 => {
                        output = CString::new("todo").unwrap();
                        type_str = String::from("table          ");
                    },
                    6 => {
                        output = CString::new("todo").unwrap();
                        type_str = String::from("function       ");
                    },
                    7 => {
                        output = CString::new((lua_touserdata(self.state, i) as usize).to_string()).unwrap();
                        type_str = String::from("userdata       ");
                    },
                    8 => {
                        output = CString::new("todo").unwrap();
                        type_str = String::from("thread         ");
                    },
                    _ => {
                        output = CString::new("unknown").unwrap();
                        type_str = String::from("unknown        ");
                    }
                }
                println!("Stack Index {}[{:?}]: {:?}", i, type_str, output);
            }
            println!("");
        }
    }

    pub fn set_global_integer(&self, name: &str, value: lua_Integer) {
        unsafe {
            lua_pushinteger(self.state, value);
            self.debug_dump();

            let c_string = CString::new(name).unwrap();
            lua_setglobal(self.state, c_string.as_ptr());
            self.debug_dump();
        }
    }

    pub fn set_global_value(&self, name: &str, index: i32) {
        unsafe {
            if index != 0 {
                lua_pushvalue(self.state, index);
                self.debug_dump();
            }

            let c_string = CString::new(name).unwrap();
            lua_setglobal(self.state, c_string.as_ptr());
            self.debug_dump();

            if index != 0 {
                lua_settop(self.state, -2);
                self.debug_dump();
            }
        }
    }

    pub fn create_table(&self) {
        unsafe{
            lua_createtable(self.state, 0, 0);
            self.debug_dump();
        }
    }

    pub fn create_metatable(&self, name: &str) {
        unsafe{
            let c_string = CString::new(name).unwrap();
            luaL_newmetatable(self.state, c_string.as_ptr());
            self.debug_dump();
        }
    }

    /// Set Table[name] = value
    /// if index == 0, make sure stack top is a table
    pub fn set_table_integer(&self, name: &str, value: &lua_Integer, index: i32) {
        unsafe {
            if index != 0 {
                lua_pushvalue(self.state, index);
                self.debug_dump();
            }
            
            lua_pushinteger(self.state, *value);
            self.debug_dump();

            let c_string = CString::new(name).expect("");
            lua_setfield(self.state, -2, c_string.as_ptr());
            self.debug_dump();

            if index != 0 {
                lua_settop(self.state, -2);
                self.debug_dump();
            }
        }
    }

    /// Set Table[name] = value
    /// if index == 0, make sure stack top is a table
    pub fn set_table_cfunction(&self, name: &str, value: &lua_CFunction, index: i32) {
        unsafe {
            if index != 0 {
                lua_pushvalue(self.state, index);
                self.debug_dump();
            }
            
            lua_pushcclosure(self.state, *value, 0);
            self.debug_dump();

            let c_string = CString::new(name).expect("");
            lua_setfield(self.state, -2, c_string.as_ptr());
            self.debug_dump();

            if index != 0 {
                lua_settop(self.state, -2);
                self.debug_dump();
            }
        }
    }

    /// Set Table.metatable = METATABLE[name]
    /// if index == 0, make sure stack top is a table
    pub fn set_metatable(&self, name: &str, index: i32) {
        unsafe {
            if index != 0 {
                lua_pushvalue(self.state, index);
                self.debug_dump();
            }

            let c_string = CString::new(name).expect("");
            luaL_setmetatable(self.state, c_string.as_ptr());
            self.debug_dump();

            if index != 0 {
                lua_settop(self.state, -2);
                self.debug_dump();
            }
        }
    }

    pub fn push_lightuserdata<T>(&self, ptr: *mut T) {
        unsafe {
            lua_pushlightuserdata(self.state, ptr as *mut c_void);
            self.debug_dump();
        }
    } 

    pub fn create_object_table<T>(&self, ptr: *mut T) {
        unsafe {
            lua_createtable(self.state, 0, 1);
            self.debug_dump();

            let c_string = CString::new("Object").unwrap();
            lua_pushstring(self.state, c_string.as_ptr());
            self.debug_dump();

            lua_pushinteger(self.state, ptr as *mut c_void as i64);
            self.debug_dump();

            lua_rawset(self.state, -3);
            self.debug_dump();
        }
    }

    pub fn run_script(&self, script: &String) {
        unsafe {
            let c_string = CString::new(script.clone()).unwrap();

            let r_value = luaL_dostring(self.state, c_string.as_ptr());
            // println!("{r_value}");
            self.debug_dump();
        }
    }

    pub fn dump_stack_raw(state: *mut lua_State) {
        unsafe {
            let stack_top = lua_gettop(state);

            println!("Dump Stack");

            for i in 1..=stack_top {
                let element_type = lua_type(state, i);
                let type_str;
                let output;
                match element_type {
                    0 => {
                        output = CString::new("nil").unwrap();
                        type_str = String::from("nil            ");
                    },
                    1 => {
                        output = CString::new(lua_toboolean(state, i).to_string()).unwrap();
                        type_str = String::from("boolean        ");
                    },
                    2 => {
                        output = CString::new((lua_touserdata(state, i) as usize).to_string()).unwrap();
                        type_str = String::from("lightuserdata  ");
                    },
                    3 => {
                        let is_integer = lua_isinteger(state, i) != 0;
                        if is_integer {
                            output = CString::new(lua_tointegerx(state, i, std::ptr::null_mut()).to_string() + &is_integer.to_string()).unwrap();
                        } else {
                            output = CString::new(lua_tonumberx(state, i, std::ptr::null_mut()).to_string() + &is_integer.to_string()).unwrap();
                        }
                        type_str = String::from("number         ");
                    },
                    4 => {
                        output = CStr::from_ptr(lua_tolstring(state, i, std::ptr::null_mut())).to_owned();
                        type_str = String::from("string         ");
                    },
                    5 => {
                        output = CString::new("todo").unwrap();
                        type_str = String::from("table          ");
                    },
                    6 => {
                        output = CString::new("todo").unwrap();
                        type_str = String::from("function       ");
                    },
                    7 => {
                        output = CString::new((lua_touserdata(state, i) as usize).to_string()).unwrap();
                        type_str = String::from("userdata       ");
                    },
                    8 => {
                        output = CString::new("todo").unwrap();
                        type_str = String::from("thread         ");
                    },
                    _ => {
                        output = CString::new("unknown").unwrap();
                        type_str = String::from("unknown        ");
                    }
                }
                println!("Stack Index {}[{:?}]: {:?}", i, type_str, output);
            }
            println!("");
        }
    }

    /// 1. get index_string [like a.b -> "b"]
    /// 2. get address of object from table
    /// 3. get __name from table's metatable
    /// 4. get getter function in rust by __name & value from getter()
    /// 5. store value to table
    /// 6. return value
    pub unsafe extern "C" fn get_distributor(state: *mut lua_State) -> c_int {
        // println!("[Rust] In get_distributor");
        // println!("Stack Num: {}", lua_gettop(state));                //  |·····| TABLE | INDEX_STR |                            PARAMS FROM LUA

        let index_string = get_funcs::get_index_string(state);
        // println!("[Rust] index_string: {}", index_string);

        let object_address = get_funcs::get_object_address(state);
        // Able to Find a Address
        if object_address != 0 {
            // println!("[Rust] Get Object Address from Lua: {:?}", object_address as *mut c_void);

            // Get Struct Name
            let struct_name = get_funcs::get_struct_name(state);
            // println!("[Rust] struct_name: {}", struct_name);

            // Get Value
            let value = get_funcs::get_value(state, object_address, &struct_name, &index_string);
            // println!("[Rust] Get Data.{:?}, {:?}", &index_string, value);

            // Check Value
            if let Value::Nil() = value {
                
            } else {
                get_funcs::push_luavalue(state, value);
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | new value |                           SET TABLE.METATABLE = MyMetatable

                // get_funcs::set_newvalue_to_table(state);
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | new value |                           SET TABLE.METATABLE = MyMetatable

                lua_rotate(state, -3, 1);
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| new value | TABLE | INDEX_STR |                           SET TABLE.METATABLE = MyMetatable

                // Clear
                lua_settop(state, 1);            
                // println!("Stack Num: {}", lua_gettop(state));         //  |·····| get_value |                                PUSH RESULT

                return 1
            }
        }
        
        // Clear
        lua_settop(state, 0);
        0
    }

    pub unsafe extern "C" fn set_distributor(state: *mut lua_State) -> c_int {
        // println!("[Rust] In set_distributor");

        // println!("Stack Num: {}", lua_gettop(state));         //  |·····| TABLE | INDEX_STR | VALUE |                           PARAMS FROM LUA

        let index_string = set_funcs::get_index_string(state);
        // println!("[Rust] index_string: {}", index_string);
        
        let object_address = set_funcs::get_object_address(state);

        // Able to Find a Address
        if object_address != 0 {
            // println!("[Rust] Get Object Address from Lua: {:?}", object_address as *mut c_void);

            // Get Struct Name
            let struct_name = set_funcs::get_struct_name(state);
            // println!("[Rust] struct_name: {}", struct_name);  

            // value to LuaValue
            let value = set_funcs::get_value_from_stack(state);

            set_funcs::set_value(state, object_address, &struct_name, &index_string, &value);
        }

        // Deal Lua Logic
        // lua_rawset(state, 1);
        lua_settop(state, 0);

        0
    }
}